package mh.map.objectset;

import imageEditor.PngImage;

import java.awt.Color;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Properties;
import java.util.Vector;

import javax.imageio.ImageIO;

import mh.entity.EnumPosition;
import mh.entity.EnumState;
import mh.entity.object.EntityItem;
import mh.entity.object.EntityObject;
import mh.map.render.RenderItem;
import mh.util.Pair;
import otxImporter.OTXImporter;

/**
 * Simple class that import an image into a Collection of <code>EntityObjects</code> satisfying the OSX format.<br>
 * OSX Means: ObjectSet XML.
 * 
 * @author Gianmarco Laggia
 * @author Alberto D'Este
 * @since 0.3
 */
public class OSXImporter implements Runnable {

	/********************************************
	 * !!!!!!!!!OBX FORMAT DECLARATION!!!!!!!!! *
	 ******************************************** 
	 * 
	 * 0----------+ 0 = id (first pixel) |imageimage| |-+ = borders |imageimage| |imageimage| |imageimage| |imageimage|
	 * +----------+
	 * 
	 * - IDENTIFIER: The format itentifies itself just looking at the first pixel (position 0,0) and comparing it with
	 * the identifier color
	 * 
	 * - BORDER: The border (drawn outside) of each image is composed by a sequence of *borderColor*. Example: Image
	 * start x : 100, 100 Image size : 50, 70 Border Positions: Top: x: 99 y: 99 -> 151 Right: x: 99 -> 171 y: 151
	 * Bottom: x: 171 y: 151 -> 99 Left: x: 171 -> 99 y: 99
	 * 
	 * - ID: The top-left pixel of each border identifies the id of the object. The status can also be saved in the
	 * alpha channel or in the last two HEX values (B or ALPHA)
	 * 
	 */

	// =================================================== //
	//                COSTOMIZABLE VARIABLES               //
	// =================================================== //
	/**
	 * Determines if the status of image has to be read from alpha channel (true value) or from the Blue channel (false)
	 */
	public static boolean			alphaActive			= false;

	/**
	 * Defines if the importation process should use multiple threads or not
	 */
	private boolean					multiThread			= true;

	private int						maxThreads			= 20;

	/**
	 * Color that identifies that the image is a OSX object
	 */
	public static final Color		identifierColor		= new Color(100, 100, 100);

	/**
	 * Color that identifies the border of the object (light purple)
	 */
	public static final Color		borderColor			= new Color(255, 0, 255, 255);

	// =================================================== //
	//                    CLASS VARIABLES                  //
	// =================================================== //

	/**
	 * Decides if there should be a debug with outputs or not
	 */
	private boolean					debug				= false;

	/**
	 * Image to scan
	 */
	private BufferedImage			img;

	/**
	 * Height of the given image
	 */
	private int						height;

	/**
	 * Width of the given image
	 */
	private int						width;

	/**
	 * Matrix of check-bits for the image (every thread that looks at the bit sets it to false)
	 */
	private boolean[][]				checkBits;

	/**
	 * The last shown bit (X value)
	 */
	private int						positionX			= 0;

	/**
	 * The last shown bit (Y value)
	 */
	private int						positionY			= 0;

	/**
	 * Threads started
	 */
	private int						startedThreads		= 0;

	/**
	 * Threads stopped
	 */
	private int						stoppedThreads		= 0;

	/**
	 * Imported objects
	 */
	private Vector<EntityObject>	objects;

	private Vector<Properties>		prop;

	private int						line				= 0;

	//=============================//
	//     ATTRIBUTES NAMES        //
	//=============================//

	private static String[]			attributes			= new String[] { "FRAMES", "FRAMES_SPEED", "DAMAGE_THRESHOLD", "HEALTH", "HEIGHT", "WEIGHT",
			"COLLECTABLE", "STACKABLE", "WALKABLE", "TYPE", "REGISTRATION", "TILES", "XPOS", "YPOS", "WIDTH", "HEIGHTPX", "STATE_TYPE", "RENDER_ID", "MASK" };

	private static String			A_FRAMES			= attributes[0];

	private static String			A_FRAMES_SPEED		= attributes[1];

	private static String			A_DAMAGE_THRESHOLD	= attributes[2];

	private static String			A_HEALTH			= attributes[3];

	private static String			A_HEIGHT			= attributes[4];

	private static String			A_WEIGHT			= attributes[5];

	private static String			A_COLLECTABLE		= attributes[6];

	private static String			A_STACKABLE			= attributes[7];

	private static String			A_WALKABLE			= attributes[8];

	private static String			A_TYPE				= attributes[9];

	private static String			A_REGISTRATION		= attributes[10];

	private static String			A_TILES				= attributes[11];

	private static String			A_X					= attributes[12];

	private static String			A_Y					= attributes[13];

	private static String			A_W					= attributes[14];

	private static String			A_H					= attributes[15];

	private static String			A_STATE_TYPE		= attributes[16];

	private static String			A_RENDER_ID			= attributes[17];

	private static String			A_MASK				= attributes[18];

	/**
	 * Constructor instantiates the class from the BufferedImage object
	 * 
	 * @param ref
	 *            The string link to the image
	 */
	public OSXImporter(String ref) {

		File f = new File(ref);

		if (f.exists()) {
			BufferedImage image = null;

			try {
				image = ImageIO.read(f);
			} catch (IOException e) {
			}

			if (OTXImporter.isOTX(image)) {

				this.img = image;
				this.height = this.img.getHeight();
				this.width = this.img.getWidth();
				this.checkBits = new boolean[this.height][this.width];
				this.objects = new Vector<EntityObject>();
				this.prop = new Vector<Properties>();
				for (int i = 0; i < this.height; i++) {
					Arrays.fill(this.checkBits[i], Boolean.TRUE);
				}
			}
		}

	}

	/**
	 * Checks the image and if it has the requested requirements returns true. Otherwise returns false
	 * 
	 * @param b
	 *            BufferedImage The image to check
	 * @return boolean
	 */
	public static boolean isOSX(BufferedImage b) {
		Color c = new Color(b.getRGB(0, 0));
		return c.equals(identifierColor);
	}

	/**
	 * Returns an array of OSXImportable objects
	 * 
	 * @return <code>Vector&lt;OSXImportable&gt;</code>
	 */
	public Vector<EntityObject> getObjects() {
		this.startThread();

		while (this.startedThreads > this.stoppedThreads) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
			}
		}

		for (Properties p : this.prop) {
			boolean found = false;
			int framesNumber = Integer.parseInt((String) p.get(A_FRAMES));
			int frameDuration = Integer.parseInt((String) p.get(A_FRAMES_SPEED));
			int state = Integer.parseInt((String) p.get(A_STATE_TYPE));
			int tileX = Integer.parseInt((String) p.get(A_X));
			int tileY = Integer.parseInt((String) p.get(A_Y));
			int tileWidth = Integer.parseInt((String) p.get(A_W));
			int tileHeight = Integer.parseInt((String) p.get(A_H));
			int renderId = Integer.parseInt((String) p.get(A_RENDER_ID));
			boolean isMask = (Integer.parseInt((String) p.get(A_MASK)) == 1 ? true : false);

			for (EntityObject obj : this.objects) {//check if the respective render (object) is already existing
				if (obj.getRender().getEntityDatabaseID() == renderId) {// if exists adds just a state to the render (object)
					obj.getRender()
							.addState(EnumState.getState(state), new Rectangle(tileX, tileY, tileWidth, tileHeight), framesNumber, frameDuration, isMask);
					found = true;
					break;
				}
			}

			if (!found) {
				//add render to the render array
				//TODO switch render types
				RenderItem r = new RenderItem(this.getImage(), renderId, EnumPosition.TOP_LEFT);
				r.addState(EnumState.getState(state), new Rectangle(tileX, tileY, tileWidth, tileHeight), framesNumber, frameDuration, isMask);
				//TODO switch entity types
				this.objects.add(new EntityItem(r, Integer.parseInt((String) p.get(A_DAMAGE_THRESHOLD)), Integer.parseInt((String) p.get(A_HEALTH)), Integer
						.parseInt((String) p.get(A_WEIGHT)), 0, false));
			}
		}

		//Remove objects with only one state of NOT_FOUND
		Vector<EntityObject> ret = new Vector<EntityObject>();

		for (EntityObject e : this.objects) {
			if (!((e.getRender().getRegisteredStates().length == 1) && e.getRender().getRegisteredStates()[0].equals(EnumState.NOT_FOUND))) {
				ret.add(e);
			}
		}

		/*
		 * istantiate object and return them as entityObject ONLY IN THIS METHOD!!!!!!
		 */
		return ret;

	}

	/**
	 * Returns the x and y of the next point to be checked
	 * 
	 * @return int[2]
	 */
	public synchronized int[] getNextPoint() {
		if ((this.positionX >= this.width) || (this.positionY >= this.height)) {
			return null;
		}

		while ((this.positionY < this.height) || (this.positionX < this.width)) {
			if (this.checkBits[this.positionX][this.positionY]) {
				break;
			}

			this.positionX++;
			if (this.positionX >= this.width) {
				this.positionX = 0;
				this.positionY++;
				if (this.positionY >= this.height) {
					break;
				}
			}
		}

		if ((this.positionX >= this.width) || (this.positionY >= this.height)) {
			return null;
		}

		this.setPointChecked(this.positionX, this.positionY);

		return new int[] { this.positionX, this.positionY };
	}

	/**
	 * Sets a specific point as checked
	 * 
	 * @param x
	 *            Position of the point
	 * @param y
	 *            Position of the point
	 */
	public synchronized void setPointChecked(int x, int y) {
		this.checkBits[x][y] = false;
	}

	/**
	 * Sets a range of points as checked
	 * 
	 * @param startX
	 *            Start of the points
	 * @param endX
	 *            End of the group of points
	 * @param y
	 *            Y position of the points
	 */
	public synchronized void setPointChecked(int startX, int endX, int y) {
		while (startX <= endX) {
			this.checkBits[startX][y] = false;
			startX++;
		}
	}

	/**
	 * Starts a new thread if multiThreading enabled or if it's the first started thread
	 * 
	 * @return boolean
	 */
	public boolean startThread() {
		if (this.multiThread || (this.startedThreads == 0)) {
			Thread t = new Thread(this);
			t.start();
			this.startedThreads++;
			return true;
		}
		return false;
	}

	/**
	 * Stops a thread and adds it to the counter
	 */
	public synchronized void setStopped() {
		this.stoppedThreads++;
		//		if(this.stoppedThreads == this.startedThreads)
		//			System.out.println("Importer finished");
	}

	/**
	 * Returns the image
	 * 
	 * @return BufferedImage
	 */
	public BufferedImage getImage() {
		return this.img;
	}

	public Pair<Integer, Integer> getIdAndState(Color c) {
		if (alphaActive) {
			return new Pair<Integer, Integer>((int) ((c.getRed() * Math.pow(256, 2)) + (c.getGreen() * Math.pow(256, 1)) + c.getBlue()), c.getAlpha());
		} else {
			return new Pair<Integer, Integer>((int) ((c.getRed() * Math.pow(256, 1)) + c.getGreen()), c.getBlue());
		}
	}

	public synchronized void addState(int stateX, int stateY, int stateW, int stateH, int renderId, int stateType) {
		Properties p = this.getTileProperties(stateX, stateY, stateW, stateH);
		p.setProperty(A_RENDER_ID, Integer.toString(renderId));
		p.setProperty(A_STATE_TYPE, Integer.toString(stateType));

		this.prop.add(p);
	}

	@Override
	public void run() {//TODO every time an object is imported all the others threads shouldn't look in pixels inside that. There should be a matrix giving the next valid pizel for the line considering already read pixels.
		int line = this.getNextLine();//get line to work in

		if ((this.startedThreads < this.maxThreads) && this.multiThread) {//if multi-thread and still threads to create, create thread
			this.startThread();
		}

		while (line < this.getImage().getHeight()) {//loop until there is no lines to look

			int xFound = -1; //will point to the first border object

			for (int i = 0; i < this.getImage().getWidth(); i++) {//will cycle the full line looking for width-top-borders
				if (PngImage.getColor(i, line, this.getImage()).equals(OSXImporter.borderColor)) {//border color found
					if (xFound == -1) {//if the border wasn't previously found set it as the first border point
						xFound = i;
					}
				} else {//current point is not border
					if ((xFound != -1) && ((i - xFound) > 2)) {//if the line is a 1px or a 2px line don't threat it as a sprite (borders of other images)
						if ( //check if the right-bottom border is a new valid line
						!(//check if there is an entering line in the upper right corner
						PngImage.getColor(i - 1, line - 1, this.getImage()).equals(OSXImporter.borderColor)
								&& !PngImage.getColor(i - 2, line - 1, this.getImage()).equals(OSXImporter.borderColor) && !PngImage.getColor(i + 1, line - 1,
								this.getImage()).equals(OSXImporter.borderColor))) {//the right line can be read
							int y;
							for (y = line; y < this.getImage().getHeight(); y++) {//read until there are no more pixels in the image or until a non-border pixel is found
								if (!PngImage.getColor(i - 1, y, this.getImage()).equals(OSXImporter.borderColor)) {//end found, add the sprite
									break;
								}
							}

							Pair<Integer, Integer> idState = this.getIdAndState(PngImage.getColor(xFound - 1, line, this.getImage())); //retrieving tileID
							this.addState(xFound, line + 1, (i - 1) - xFound, (y - 1) - (line + 1), idState.getLeft(), idState.getRight()); //adding the tile
							if (this.debug) {
								System.out.println("Object found: " + xFound + ", " + (line + 1) + ", " + ((i - 1) - xFound) + ", " + ((y - 1) - (line + 1))
										+ " ID : " + idState.getLeft() + " STATE: " + idState.getRight());
							}
						}
					}
					xFound = -1;//image found, reset the found variable
				}
			}

			line = this.getNextLine();//line ended, read a new line and restart cycle

		}

		this.setStopped(); // all lines read, block the execution
	}

	/**
	 * Returns the properities of the given tile
	 * 
	 * @param x
	 * @param y
	 * @param w
	 * @param h
	 * @return
	 */
	private Properties getTileProperties(int x, int y, int w, int h) {
		Properties p = new Properties();

		for (String s : attributes) {
			//preset of all attributes values to 0
			p.setProperty(s, Integer.toString(0));
		}
		p.setProperty(A_X, Integer.toString(x));
		p.setProperty(A_Y, Integer.toString(y));
		p.setProperty(A_W, Integer.toString(w));
		p.setProperty(A_H, Integer.toString(h));
		p.setProperty(A_FRAMES, Integer.toString(1));

		for (int i = 0; i < h; i++) {
			Color c = PngImage.getColor(x - 1, y + i, this.img);
			if (borderColor.equals(c)) { // the first time I find a non-coded block the script
				return p;
			}
			switch (c.getBlue()) {
				case 1:
					//R = TYPE of the entity
					//G = 0 if is a full state, 1 if is a mask
					p.setProperty(A_TYPE, Integer.toString(c.getRed()));
					p.setProperty(A_MASK, Integer.toString(c.getGreen()));
					break;
				case 2:
					//R = FRAMES number
					//G = SPEED of animation if more frames
					p.setProperty(A_FRAMES, Integer.toString(c.getRed()));
					p.setProperty(A_FRAMES_SPEED, Integer.toString(c.getGreen()));
					break;
				case 3:
					//RG = HEALTH
					p.setProperty(A_HEALTH, Integer.toString(c.getRGB() & 0x00FFFF00));
					break;
				case 4:
					//R = DAMAGE_THRESHOLD
					//G = HEIGHT of the entity
					p.setProperty(A_DAMAGE_THRESHOLD, Integer.toString(c.getRed()));
					p.setProperty(A_HEIGHT, Integer.toString(c.getGreen()));
					break;
				case 5:
					//RG = WEIGHT of the entity
					p.setProperty(A_WEIGHT, Integer.toString(c.getRGB() & 0x00FFFF00));
					break;
				case 6:
					//RG = 0...0, WALKABLE, STACKABLE, COLLECTABLE
					int flags = (c.getRed() * 256) + c.getGreen(); //int flags = c.getRGB() & 0x00FFFF00;
					p.setProperty(A_COLLECTABLE, Integer.toString(getBit(flags, 0)));//flags & 0x000001));
					p.setProperty(A_STACKABLE, Integer.toString(getBit(flags, 1)));//(flags & 0x000002) >> 1));
					p.setProperty(A_WALKABLE, Integer.toString(getBit(flags, 2)));//(flags & 0x000004) >> 2));
					break;
				case 7:
					//R = REGISTRATION (position of the render from the EnumPosition)
					//G = TILES (numebr of tiles)
					p.setProperty(A_REGISTRATION, Integer.toString(c.getRed()));
					p.setProperty(A_TILES, Integer.toString(c.getGreen()));
					break;
				default:
					break;
			}

		}
		return p;
	}

	/**
	 * Returns the specified bit in the n-bit number
	 * 
	 * @param number
	 * @param bitNumber
	 * @return
	 */
	public static int getBit(int number, int bitNumber) {
		//System.out.println(number + " (" + bitNumber + ") = " + ((number & (int)Math.pow(2, bitNumber)) >> bitNumber));
		return (number & (int) Math.pow(2, bitNumber)) >> bitNumber;
	}

	/**
	 * Sets if the instance will work with multi-threading or not
	 * 
	 * @param value
	 */
	public void setMultiThread(boolean value) {
		this.multiThread = value;
	}

	/**
	 * Returns the next line to be checked from the thread
	 * 
	 * @return
	 */
	public int getNextLine() {
		return this.line++;
	}

}